package Parser;

import java.io.PrintWriter;
import java.util.ArrayList;
import scanner.*;
import scanner.Token.TokenType;
import static scanner.Token.TokenType.INT_TOKEN;
import static scanner.Token.TokenType.VOID_TOKEN;

/**
 * Parser of the C- language
 * @author Tyler Helmuth + Max Muhlenkamp
 */
public class CMinusParser implements Parser {
    private Scanner sc;
    private Program ast;
    public static PrintWriter oFile;
    public CMinusParser(Scanner sc){
        this.sc = sc;
    }
    @Override
    public Program parse() {
        ast = parseProgram();
        return ast;
    }
    
    @Override
    public void printTree(PrintWriter output) {
        oFile = output;
        ast.print();
    }
    /**
     * PARSE FUNCTIONS 
     */
    public Program parseProgram(){
        Program myProgram = new Program();

        Token currToken = sc.viewNextToken();
        while(currToken.getType() != Token.TokenType.EOF_TOKEN){
            switch(currToken.getType()){
                case INT_TOKEN:
                case VOID_TOKEN:
                    myProgram.addDecl(parseDecl());
                    break;
                default:
                    throw new ParseException("Expected type specifier but got"
                            + currToken.getType() + "\n");
            }
            currToken = sc.viewNextToken();
        }
        return myProgram;
    }
    
    
    public Decl parseDecl(){
        Decl result = null;
        Token idToken;
        Token typeToken;
        typeToken = sc.getNextToken();
        idToken = sc.getNextToken();
        if(sc.viewNextToken().getType() == Token.TokenType.OPAREN_TOKEN) /* Fundecl */
        {
           result = parseFundecl(new Identifier((String)idToken.getData(), null), typeToken.getType());
        } /* Vardecl */
        else if (sc.viewNextToken().getType() == Token.TokenType.OBRACE_TOKEN
                || sc.viewNextToken().getType() == Token.TokenType.SEMICOLON_TOKEN){
            result = parseVardecl(new Identifier((String)idToken.getData(), null), typeToken.getType());
        }
        return result;
    }
    
    public Fundecl parseFundecl(Identifier id, Token.TokenType type){
        Fundecl result;
        CompoundStmt compound;
        matchToken(Token.TokenType.OPAREN_TOKEN);
        result = new Fundecl(id, type);
        Token currToken = sc.getNextToken();
        while(currToken.getType() != Token.TokenType.CPAREN_TOKEN){
            Token pId, pType;
            pType = currToken;
            if(pType.getType() == Token.TokenType.INT_TOKEN){
                boolean isArray = false; // if we find bracket after id it's an array of type int
                currToken = sc.getNextToken();
                pId = currToken;
                if(sc.viewNextToken().getType() == Token.TokenType.OBRACE_TOKEN){
                    isArray = true;
                    matchToken(Token.TokenType.OBRACE_TOKEN);
                    matchToken(Token.TokenType.CBRACE_TOKEN);
                }
                result.addParam(new Param(new Identifier((String)pId.getData(), null), isArray));
                currToken = sc.getNextToken();
                if(currToken.getType() == TokenType.COMMA_TOKEN){
                    currToken = sc.getNextToken();
                    
                    if(currToken.getType() == TokenType.CPAREN_TOKEN){
                        throw new ParseException("Expected another parameter in fundecl.");
                    }
                }
            } else if (pType.getType() == Token.TokenType.VOID_TOKEN){
                //no params
                currToken = sc.getNextToken();
            }
            else{
                throw new ParseException("Error when parsing fundecl. No int or void");
            }
        }
        compound = parseCompoundStmt();
        result.setCompoundStmt(compound);
        return result;
    }

    
    public Vardecl parseVardecl(Identifier id, Token.TokenType type){
        Vardecl result;
        result = new Vardecl(id, type);
        if(sc.viewNextToken().getType() == Token.TokenType.OBRACE_TOKEN){
            Token arraySize;
            matchToken(Token.TokenType.OBRACE_TOKEN);
            arraySize = sc.getNextToken();
            result.setArraySize((int)arraySize.getData());
            matchToken(Token.TokenType.CBRACE_TOKEN);
        }
        matchToken(Token.TokenType.SEMICOLON_TOKEN);

        return result;
    }

    public CompoundStmt parseCompoundStmt(){
        matchToken(TokenType.OCURLY_TOKEN);
        ArrayList<Vardecl> localDecls = null;
        ArrayList<Statement> statements = null;
        CompoundStmt result;
        localDecls = parseLocalDecls();
        statements = parseStatementList();
        result = new CompoundStmt(localDecls, statements);
        matchToken(TokenType.CCURLY_TOKEN);
        return result;
    }
       
    public ArrayList<Vardecl> parseLocalDecls(){
        ArrayList<Vardecl> result = new ArrayList<>();
        Token currToken = sc.viewNextToken();
        while(currToken.getType() == Token.TokenType.INT_TOKEN){
            currToken = sc.getNextToken();
            Vardecl newVar;
            Token.TokenType type = currToken.getType();
            Token idToken = sc.getNextToken();
            Identifier id = new Identifier((String)idToken.getData(), null);
            newVar = parseVardecl(id, type);
            result.add(newVar);
            currToken = sc.viewNextToken();
        }
        return result;
    }
    
    // Parse statement list
    public ArrayList<Statement> parseStatementList(){
        ArrayList<Statement> result = new ArrayList<>();
        
        TokenType next = sc.viewNextToken().getType();
        while(next == TokenType.OPAREN_TOKEN || next == TokenType.SEMICOLON_TOKEN 
                || next == TokenType.OCURLY_TOKEN || next == TokenType.IF_TOKEN 
                || next == TokenType.WHILE_TOKEN || next == TokenType.RETURN_TOKEN
                || next == TokenType.ID_TOKEN){
            //sc.getNextToken();
            next = sc.viewNextToken().getType();
           
            result.add(parseStatement());
            next = sc.viewNextToken().getType();
        }
        
        return result;
    }
    
    // Parse Statement
    public Statement parseStatement(){
        TokenType next = sc.viewNextToken().getType();
        
        if(next == TokenType.OPAREN_TOKEN || next == TokenType.ID_TOKEN || 
                next == TokenType.NUM_TOKEN || next == TokenType.SEMICOLON_TOKEN){
            return parseExprStmt();
        }
        else if(next == TokenType.OCURLY_TOKEN){
            return parseCompoundStmt();
        }
        else if(next == TokenType.IF_TOKEN){
            return parseSelectionStmt();
        }
        else if(next == TokenType.WHILE_TOKEN){
            return parseIterationStmt();
        }
        else if(next == TokenType.RETURN_TOKEN){
            return parseReturnStmt();
        }
        else{
            throw new ParseException("Error when parsing statement. Invalid next token");
        }
    }
    
    // parse expression statement
    public ExpressionStmt parseExprStmt(){
        TokenType next = sc.viewNextToken().getType();
        Expression expr = null;
        
        if(next == TokenType.OPAREN_TOKEN || next == TokenType.NUM_TOKEN || next == TokenType.ID_TOKEN){
            expr = parseExpression();
        }
        
        if(sc.getNextToken().getType() != TokenType.SEMICOLON_TOKEN){
            throw new ParseException("Error when parsing expression statement. Missing semicolon.");
        }
        
        return new ExpressionStmt(expr);
    }
    
    // parse selection statement
    public SelectionStmt parseSelectionStmt(){
        if(sc.getNextToken().getType() != TokenType.IF_TOKEN){
            throw new ParseException("Error when parsing selection statment. Missing first IF token.");
        }
        
        if(sc.getNextToken().getType() != TokenType.OPAREN_TOKEN){
            throw new ParseException("Error when parsing selection statment. Missing first open paren token.");
        }
        
        Expression expr = parseExpression();
        
        if(sc.getNextToken().getType() != TokenType.CPAREN_TOKEN){
            throw new ParseException("Error when parsing selection statment. Missing close paren token.");
        }
        
        Statement stmt = parseStatement();
        Statement elseStmt = null;
        if(sc.viewNextToken().getType() == TokenType.ELSE_TOKEN){
            sc.getNextToken();
            elseStmt = parseStatement();
        }
        
        return new SelectionStmt(expr, stmt, elseStmt);
    }
    
    // parse iteration statement
    public IterationStatement parseIterationStmt(){
        if(sc.getNextToken().getType() != TokenType.WHILE_TOKEN){
            throw new ParseException("Error when parsing iteration statement. Missing while token.");
        }
        
        if(sc.getNextToken().getType() != TokenType.OPAREN_TOKEN){
            throw new ParseException("Error when parsing iteration statement. Missing open paren token.");
        }
        
        Expression expr = parseExpression();
        
        if(sc.getNextToken().getType() != TokenType.CPAREN_TOKEN){
            throw new ParseException("Error when parsing iteration statement. Missing close paren token.");
        }
        
        Statement stmt = parseStatement();
        
        return new IterationStatement(expr, stmt);
    }
    
    // parse return statement
    public ReturnStmt parseReturnStmt(){
        if(sc.getNextToken().getType() != TokenType.RETURN_TOKEN){
            throw new ParseException("Error when parsing return statement. Missing return token.");
        }
        
        Expression expr = null;
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.OPAREN_TOKEN || next == TokenType.NUM_TOKEN || next == TokenType.ID_TOKEN){
            expr = parseExpression();
        }
        
        if(sc.getNextToken().getType() != TokenType.SEMICOLON_TOKEN){
            throw new ParseException("Error when parsing return statement. Missing semicolon token.");
        }
        
        return new ReturnStmt(expr);
    }
    
    // parse Expression
    public Expression parseExpression(){
        if(sc.viewNextToken().getType() == TokenType.OPAREN_TOKEN){
            sc.getNextToken();
            Expression factor = parseExpression();
            
            if(sc.getNextToken().getType() != TokenType.CPAREN_TOKEN){
                throw new ParseException("Error when parsing Expression");
            }
            
            return parseSEPrime(factor);
        }
        else if(sc.viewNextToken().getType() == TokenType.NUM_TOKEN){
            NumExpression num = new NumExpression((int)sc.getNextToken().getData());
            return parseSEPrime(num);
        }
        else if(sc.viewNextToken().getType() == TokenType.ID_TOKEN){
            Identifier id = new Identifier((String)sc.getNextToken().getData(), null);
            return parseExprPrime(id);
        }
        else{
            throw new ParseException("Error when parsing Expression. Invalid next token.");
        }
    }
    
    
    // Parse Expression prime
    public Expression parseExprPrime(Identifier iden){
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.ASSIGN_TOKEN){
            sc.getNextToken();
            Identifier id = (Identifier)iden;
            return new AssignExpression(id, parseExpression());
        }
        else if(next == TokenType.OPAREN_TOKEN){
            sc.getNextToken();
            ArrayList<Expression> args = parseArgs();
            if(sc.getNextToken().getType() != TokenType.CPAREN_TOKEN){
                throw new ParseException("Error when parsing Expression Prime.");
            }
            return parseSEPrime(new CallExpression(iden, args));
        }
        else if(next == TokenType.OBRACE_TOKEN){
            sc.getNextToken();
            iden.location = parseExpression();
            return parseExprDPrime(iden);
        }
        // if the token is in the first or follow set of Expression Prime
        else if(next == TokenType.MULT_TOKEN || next == TokenType.DIV_TOKEN || 
                next == TokenType.PLUS_TOKEN || next == TokenType.MINUS_TOKEN || 
                next == TokenType.LTE_TOKEN || next == TokenType.LT_TOKEN || 
                next == TokenType.GTE_TOKEN || next == TokenType.GT_TOKEN || 
                next == TokenType.EQUALITY_TOKEN || next == TokenType.NOT_EQUAL_TOKEN || 
                next == TokenType.COMMA_TOKEN || next == TokenType.CBRACE_TOKEN || 
                next == TokenType.CPAREN_TOKEN ||next == TokenType.SEMICOLON_TOKEN){
            return parseSEPrime(iden);
        }
        else{
            throw new ParseException("Error when parsing Expression Prime. Invalid Next Token");
        }
    }
    
    // Parse Expression Double Prime
    public Expression parseExprDPrime(Identifier iden){
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.ASSIGN_TOKEN){
            return new AssignExpression(iden, parseExpression());
        }
        // if the token is in the first or follow set of Expression Prime
        else if(next == TokenType.MULT_TOKEN || next == TokenType.DIV_TOKEN || 
                next == TokenType.PLUS_TOKEN || next == TokenType.MINUS_TOKEN || 
                next == TokenType.LTE_TOKEN || next == TokenType.LT_TOKEN || 
                next == TokenType.GTE_TOKEN || next == TokenType.GT_TOKEN || 
                next == TokenType.EQUALITY_TOKEN || next == TokenType.NOT_EQUAL_TOKEN || 
                next == TokenType.COMMA_TOKEN || next == TokenType.CBRACE_TOKEN || 
                next == TokenType.CPAREN_TOKEN ||next == TokenType.SEMICOLON_TOKEN){
            return parseSEPrime(iden);
        }
        else{
            throw new ParseException("Error when parsing Expression Double Prime. Invalid Next Token");
        }
    }
    
    // Parse Simple Expression Prime
    public Expression parseSEPrime(Expression factor){
        Expression adExpr = parseAddExpr(factor);
        
        TokenType next = sc.viewNextToken().getType();
        // check if the next token is in the first set of relop
        if(next == TokenType.LTE_TOKEN || next == TokenType.LT_TOKEN || 
                next == TokenType.GTE_TOKEN || next == TokenType.GT_TOKEN || 
                next == TokenType.EQUALITY_TOKEN || next == TokenType.NOT_EQUAL_TOKEN){
            next = sc.getNextToken().getType();
            return new BinaryExpression(adExpr, next, parseAddExpr(null));
        }
        return adExpr;
    }
    
    
    // Parse Additive Expression
    public Expression parseAddExpr(Expression factor){
        Expression term = parseTerm(factor);
        
        // check if the next token is in the first set of addop
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.PLUS_TOKEN || next == TokenType.MINUS_TOKEN){
            next = sc.getNextToken().getType();
            BinaryExpression leftExpr = new BinaryExpression(term, next, parseTerm(null));
            
            // continue finding addop terms
            while(sc.viewNextToken().getType() == TokenType.PLUS_TOKEN || sc.viewNextToken().getType() == TokenType.MINUS_TOKEN){
                BinaryExpression fullExpr = new BinaryExpression(leftExpr, sc.getNextToken().getType(), parseTerm(null));
                leftExpr = fullExpr;
            }
            
            return leftExpr;
        }
        
        return term;
    }
    
    
    // Parse Term
    public Expression parseTerm(Expression factor){
        if(factor == null){
            factor = parseFactor();
        }
        
        // check if the next token is in the first set of mulop
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.MULT_TOKEN || next == TokenType.DIV_TOKEN){
            next = sc.getNextToken().getType();
            
            BinaryExpression leftExpr = new BinaryExpression(factor, next, parseFactor());
            
            while(sc.viewNextToken().getType() == TokenType.MULT_TOKEN || sc.viewNextToken().getType() == TokenType.DIV_TOKEN){
                BinaryExpression fullExpr = new BinaryExpression(leftExpr, sc.getNextToken().getType(), parseFactor());
                leftExpr = fullExpr;
            }
            
            return leftExpr;
        }
        
        return factor;
    }
    
    // Parse Factor
    public Expression parseFactor(){
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.OPAREN_TOKEN){
            next = sc.getNextToken().getType();
            Expression expr = parseExpression();
            if(next != TokenType.CPAREN_TOKEN){
                throw new ParseException("Error when parsing factor.");
            }
            
            return expr;
        }
        else if(next == TokenType.SEMICOLON_TOKEN){
            return null;
        }
        else if(next == TokenType.ID_TOKEN){
            Identifier id = new Identifier((String)sc.getNextToken().getData(), null);
            return parseVarCall(id);
        }
        else if(next == TokenType.NUM_TOKEN){
            return new NumExpression((int)sc.getNextToken().getData());
        }
        else{
            throw new ParseException("Error when parsing factor. Invalid next token.");
        }
    }
    
    // Parse Varcall'
    public Expression parseVarCall(Identifier iden){
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.OBRACE_TOKEN){
            sc.getNextToken();
            iden.location = parseExpression();
            return iden;
        }
        else if(next == TokenType.OPAREN_TOKEN){
            return new CallExpression(iden, parseArgs());
        }
        else{
            return iden;
        }
    }
    
    // parse args
    public ArrayList<Expression> parseArgs(){
        TokenType next = sc.viewNextToken().getType();
        if(next == TokenType.OPAREN_TOKEN || next == TokenType.NUM_TOKEN || next == TokenType.ID_TOKEN){
            return parseArgList();
        }
        
        return null;
    }
    
    // Parse arg-list
    public ArrayList<Expression> parseArgList(){
        ArrayList<Expression> list = new ArrayList<Expression>();
        
        list.add(parseExpression());
        
        while(sc.viewNextToken().getType() == TokenType.COMMA_TOKEN){
            sc.getNextToken();
            list.add(parseExpression());
        }
        
        return list;
    }
        
    /*****************************************************************
     * UTILITY FUNCTIONS
     ****************************************************************/
    private void matchToken(Token.TokenType type){
        Token testToken = sc.getNextToken();
        if(testToken.getType() != type){
            throw new ParseException("Expected non terminal: " + type.name() + 
                    " but got: " + testToken.getType().name());
        }
    }
}
